package script.grammar;

import data.analysis.Analyzer.Filter;
import data.Cell;
import data.Cluster;
import data.Focus;
import event.DebugLogger;
import event.ErrorLogger;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaFileObject;

import org.antlr.runtime.ANTLRInputStream;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.RuleReturnScope;
import org.antlr.runtime.TokenRewriteStream;
import org.antlr.runtime.tree.CommonTree;
import org.antlr.runtime.tree.CommonTreeNodeStream;
import org.antlr.stringtemplate.StringTemplate;
import org.antlr.stringtemplate.StringTemplateGroup;

import script.grammar.symbol.ArrayType;
import script.grammar.symbol.BaseScope;
import script.grammar.symbol.ClassSymbol;
import script.grammar.symbol.FieldSymbol;
import script.grammar.symbol.NativeType;
import script.grammar.symbol.Scope;
import script.grammar.symbol.SymbolTable;
import script.grammar.symbol.VariableSymbol;

import compiler.ScriptCompiler;
import compiler.ScriptCompilerException;

/**
 * A translator for filter scripts. Used for converting filter script source code to 
 * compiled and instantiated {@link Filter} objects.
 * @author Charles
 *
 */
public class FilterTranslator {
	public static final int CODE_MODE_CLASS = 0,
							CODE_MODE_BODY = 1;
	
	private static final LocScriptASTAdaptor adaptor = new LocScriptASTAdaptor();
	
	private static final String PACKAGE_NAME = "script.runtime";
	
	private static final Set<Class<?>> legalTypes = initLegalTypes();
	
	private DebugLogger d;
	private ErrorLogger e;
	
	/**
	 * Creates a new translator.
	 * @param d the {@link DebugLogger} to use.
	 * @param e the {@link ErrorLogger} to use.
	 */
	public FilterTranslator(DebugLogger d, ErrorLogger e) {
		this.d = d;
		this.e = e;
	}
	
	/**
	 * Creates a new filter object defined by source code. The new object's class type extends 
	 * {@link Filter}<code>&#060T&#062</code>.
	 * @param in a stream initialized to the beginning of the source code.
	 * @param type the class type to which the filter should be parameterized.
	 * @param className the name of the class of the new filter object.
	 * @return an object that extends <code>Filter&#060T&#062</code>.
	 * @throws IOException if there is a problem reading from the input stream.
	 */
	public <T> Filter<T> instantiateFilter(InputStream in, Class<T> type, String className) throws IOException {
		if (!legalTypes.contains(type)) {
			throw new IllegalArgumentException(type.getName() + " is not supported.");
		}
		
    	Class<Filter<T>> classObj = compileClass(in, type, className);
    	if (classObj == null) {
    		return null;
    	}
    	try {
			return classObj.newInstance();
		} catch (InstantiationException e) {
			this.e.logErrorMsg("Error: Could not instantiate compiled filter object.");
		} catch (IllegalAccessException e) {
			this.e.logErrorMsg("Error: Could not instantiate compiled filter object.");
		}
    	return null;
	}
	
	/**
	 * Creates a new class defined by source code that extends {@link Filter}<code>&#060T&#062</code>.
	 * @param in a stream initialized to the beginning of the source code.
	 * @param type the class type to which the filter should be parameterized.
	 * @param className the name of the new class.
	 * @return a class that extends <code>Filter&#060T&#062</code>.
	 * @throws IOException if there is a problem reading from the input stream.
	 */
	public <T> Class<Filter<T>> compileClass(InputStream in, Class<T> type, String className) throws IOException {
		if (!legalTypes.contains(type)) {
			throw new IllegalArgumentException(type.getName() + " is not supported.");
		}
		
		String compiled = buildTargetCode(in, type, className, CODE_MODE_CLASS);
    	if (compiled == null) {
    		return null;
    	}
    	
    	d.logDebugMsg("Compiling filter script...");
    	final DiagnosticCollector<JavaFileObject> diags = new DiagnosticCollector<JavaFileObject>();
    	
    	ScriptCompiler<Filter<T>> compiler = 
    			new ScriptCompiler<Filter<T>>(ClassLoader.getSystemClassLoader());
    	try {
			Class<Filter<T>> obj = compiler.compile(PACKAGE_NAME + "." + className, 
					compiled, diags, Filter.class);
			d.logDebugMsg("...done.");
			return obj;
		} catch (ScriptCompilerException e) {
			for (Diagnostic<? extends JavaFileObject> diag : e.getDiagnostics().getDiagnostics()) {
				this.e.logErrorMsg("Error: " + diag.getMessage(null));
			}
		}
    	return null;
	}
	
	/**
	 * Compiles script source code and generates java source code defining a new class that extends 
	 * {@link Filter}<code>&#060T&#062</code>.
	 * <p>This method can generate two different kinds of output depending on which mode is specified 
	 * in the <b>mode</b> parameter:
	 * <ul>
	 * <li><code>FilterTranslator.CODE_MODE_CLASS</code>: generates the entire class definition as it 
	 * would appear in a .java file.
	 * <li><code>FilterTranslator.CODE_MODE_BODY</code>: generates <i>only</i> the body of the class. 
	 * Useful for embedding the output in another pre-defined source code file. The <b>className</b> 
	 * parameter is ignored if this mode is used.
	 * </ul>
	 * @param in a stream initialized to the beginning of the script source code.
	 * @param type the class type to which the defined filter should be parameterized.
	 * @param className the name of the new defined class.
	 * @param mode the type of output to generate.
	 * @return java source code compiled from script source code.
	 * @throws IOException if there is a problem reading from the input stream.
	 */
	public <T> String buildTargetCode(InputStream in, Class<T> type, String className, int mode) throws IOException {
		if (!legalTypes.contains(type)) {
			throw new IllegalArgumentException(type.getName() + " is not supported.");
		}
		
		CommonTreeNodeStream nodes = parse(in, type);
        if (nodes == null) {
        	return null;
        }
    	return genCode(nodes, type, className, mode);
	}
	
	protected <T> CommonTreeNodeStream parse(InputStream in, Class<T> type) throws IOException {
		d.logDebugMsg("Parsing script source...");
		CharStream input = new ANTLRInputStream(in);
        LocScriptLexer lex = new LocScriptLexer(input);
        final TokenRewriteStream tokens = new TokenRewriteStream(lex);
        LocScriptParser p = new LocScriptParser(tokens);
        p.setTreeAdaptor(adaptor);
        RuleReturnScope r = null;
        
		try {
			r = p.start();
			if (lex.getNumberOfSyntaxErrors() + p.getNumberOfSyntaxErrors() > 0) {
				return null;
			}
			d.logDebugMsg("...AST generated...running semantic analysis...");
			
			CommonTree t = (CommonTree)r.getTree();
	        CommonTreeNodeStream nodes = new CommonTreeNodeStream(t);
	        nodes.setTokenStream(tokens);
	        nodes.setTreeAdaptor(adaptor);
	        SymbolTable symbolTable = initSymbolTable(d, e, type);
	        
	        LocScriptCheckPass pass = new LocScriptCheckPass(nodes, symbolTable);
	        e.resetCount();
	        pass.downup(t);
	        if (e.getCount() == 0) {
	        	d.logDebugMsg("...done.");
	        	return nodes;
	        } else {
	        	return null;
	        }
		} catch (RecognitionException e1) {
			e1.printStackTrace();
			return null;
		}
	}
	
	protected <T> String genCode(CommonTreeNodeStream nodes, Class<T> type, String className, int mode) {
		d.logDebugMsg("Generating Java target code...");
		InputStream templIn = ClassLoader.getSystemClassLoader().getResourceAsStream("script/grammar/LocScriptTempl.stg");
    	StringTemplateGroup templates = new StringTemplateGroup(new InputStreamReader(templIn));
    	
    	LocScriptGen gen = new LocScriptGen(nodes, type, className);
    	gen.setTemplateLib(templates);
    	StringTemplate template;
		try {
			switch (mode) {
			case CODE_MODE_CLASS: template = (StringTemplate)gen.startFilter().getTemplate(); break;
			case CODE_MODE_BODY: template = (StringTemplate)gen.startBody().getTemplate(); break;
			default:
				throw new IllegalArgumentException("Unrecognized mode: " + mode);
			}
			
		} catch (RecognitionException e1) {
			e1.printStackTrace();
			return null;
		}
    	d.logDebugMsg("...done:\n\n" + template + "\n");
    	return template.toString();
	}
	
	private <T> SymbolTable initSymbolTable(DebugLogger d, ErrorLogger e, Class<T> type) {
		Scope global = new BaseScope("Global Scope", null);
		ClassSymbol cluster = new ClassSymbol("Cluster", global);
		global.define(cluster);
		ClassSymbol cell = new ClassSymbol("Cell", global);
		global.define(cell);
		ClassSymbol slide = new ClassSymbol("Slide", global);
		global.define(slide);
		ClassSymbol focus = new ClassSymbol("Focus", global);
		global.define(focus);
		
		cluster.define(new FieldSymbol("nLbl", NativeType.INT, cluster, "getNumLabels()"));
		cluster.define(new FieldSymbol("isMem", NativeType.BOOLEAN, cluster, "isMembraneAssoc()"));
		cluster.define(new FieldSymbol("parent", cell, cluster, "getParent()"));
		
		cell.define(new FieldSymbol("nLbl", NativeType.INT, cell, "getTotalNumLabels()"));
		cell.define(new FieldSymbol("nCl", NativeType.INT, cell, "getClusters().size()"));
		cell.define(new FieldSymbol("nMemCl", NativeType.INT, cell, "getMembraneClusters().size()"));
		cell.define(new FieldSymbol("nCytoCl", NativeType.INT, cell, "getCytoplasmicClusters().size()"));
		cell.define(new FieldSymbol("cls", new ArrayType(cluster), cell, "getClusters()"));
		cell.define(new FieldSymbol("parent", slide, cell, "getParent()"));
		
		slide.define(new FieldSymbol("id", NativeType.INT, slide, "getId()"));
		slide.define(new FieldSymbol("cells", new ArrayType(cell), slide, "getCells()"));
			
		cell.define(new FieldSymbol("nFoci", NativeType.INT, cell, "getFoci().size()"));
		cell.define(new FieldSymbol("foci", new ArrayType(focus), cell, "getFoci()"));
			
		focus.define(new FieldSymbol("cl", cluster, focus, "getCluster()"));
		
		ClassSymbol classScope = null;
		if (type.equals(Cluster.class)) {
			classScope = cluster;
		} else if (type.equals(Cell.class)) {
			classScope = cell;
		} else if (type.equals(Focus.class)) {
			classScope = focus;
		}
		
		SymbolTable table = new SymbolTable(global, classScope, d, e);
		table.getLocalScope().define(new VariableSymbol("this", classScope) {
			@Override
			public String getAlias() {
				return "_this";
			}
		});
		
		return table;
	}
	
	private static Set<Class<?>> initLegalTypes() {
		Set<Class<?>> set = new HashSet<Class<?>>();
		set.add(Cluster.class);
		set.add(Cell.class);
		set.add(Focus.class);
		return set;
	}
}
